﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom;
using System.Reflection;
using System.CodeDom.Compiler;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace NodeService.Endpoints
{
    public static class StrongTypedNodeEndpointClientBuilder
    {
        private static Dictionary<Type, Type> clientImplementations = new Dictionary<Type, Type>();

        private static CodeExpression PassParameterNames(MethodInfo methodInfo)
        {
            return new CodeArrayCreateExpression(
                typeof(string),
                methodInfo
                    .GetParameters()
                    .Select(p => new CodePrimitiveExpression(p.Name))
                    .ToArray()
                );
        }

        private static CodeExpression PassParameterValues(MethodInfo methodInfo)
        {
            return new CodeArrayCreateExpression(
                typeof(object),
                methodInfo
                    .GetParameters()
                    .Select(p => new CodeArgumentReferenceExpression(p.Name))
                    .ToArray()
                );
        }

        private static CodeExpression InvokeExecute(MethodInfo methodInfo)
        {
            CodeExpression execution = new CodeMethodInvokeExpression(
                new CodeThisReferenceExpression(),
                "Execute",
                new CodeExpression[] { 
                    new CodePrimitiveExpression(methodInfo.Name),
                    new CodeTypeOfExpression(methodInfo.ReturnType),
                    PassParameterNames(methodInfo),
                    PassParameterValues(methodInfo),
                    }
                .ToArray()
                );
            return execution;
        }

        private static CodeExpression InvokeExecuteTask(MethodInfo methodInfo)
        {
            CodeExpression execution = new CodeMethodInvokeExpression(
                methodInfo.ReturnType == typeof(Task) ?
                    new CodeMethodReferenceExpression(
                        new CodeThisReferenceExpression(),
                        "ExecuteTask"
                        ) :
                    new CodeMethodReferenceExpression(
                        new CodeThisReferenceExpression(),
                        "ExecuteTask",
                        new CodeTypeReference(methodInfo.ReturnType.GetGenericArguments()[0])
                        ),
                new CodeExpression[] { 
                    new CodePrimitiveExpression(methodInfo.Name),
                    PassParameterNames(methodInfo),
                    PassParameterValues(methodInfo),
                    }
                .ToArray()
                );
            return execution;
        }

        private static Type CreateClientImplementation(Type interfaceType)
        {
            Type clientType = null;
            if (!clientImplementations.TryGetValue(interfaceType, out clientType))
            {
                Type callbackType = interfaceType.FindInterfaces(
                    (t, o) => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDuplexNodeEndpointClient<>),
                    null
                    )
                    .FirstOrDefault();

                CodeDomProvider codedomProvider = CodeDomProvider.CreateProvider("CSharp");
                string namespaceName = "NodeService.StringTypedNodeEndpointClientAutoGeneratedClients";
                string typeName = "ImplementationOf" + interfaceType.Name;

                CodeCompileUnit codeCompileUnit = new CodeCompileUnit();
                {
                    CodeNamespace codeNamespace = new CodeNamespace(namespaceName);
                    codeCompileUnit.Namespaces.Add(codeNamespace);

                    CodeTypeDeclaration clientDeclaration = new CodeTypeDeclaration(typeName);
                    if (callbackType == null)
                    {
                        clientDeclaration.BaseTypes.Add(typeof(StrongTypedNodeEndpointClient));
                    }
                    else
                    {
                        clientDeclaration.BaseTypes.Add(typeof(StrongTypedNodeEndpointClient<>).MakeGenericType(callbackType.GetGenericArguments()[0]));
                    }
                    clientDeclaration.BaseTypes.Add(interfaceType);
                    codeNamespace.Types.Add(clientDeclaration);

                    CodeConstructor clientConstructor = new CodeConstructor();
                    clientDeclaration.Members.Add(clientConstructor);
                    clientConstructor.Attributes = MemberAttributes.Public;
                    clientConstructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(INodeEndpointClientProvider), "provider"));
                    clientConstructor.BaseConstructorArgs.Add(new CodeArgumentReferenceExpression("provider"));
                    clientConstructor.Statements.Add(
                        new CodeExpressionStatement(
                            new CodeMethodInvokeExpression(
                                new CodeThisReferenceExpression(),
                                "Initialize",
                                new CodeTypeOfExpression(interfaceType)
                                )
                            )
                        );

                    var methodInfos = GetMethodInfos(interfaceType);

                    foreach (var methodInfo in methodInfos)
                    {
                        CodeMemberMethod clientMethod = new CodeMemberMethod();
                        clientDeclaration.Members.Add(clientMethod);
                        clientMethod.Attributes = MemberAttributes.Public;
                        clientMethod.Name = methodInfo.Name;
                        clientMethod.ImplementationTypes.Add(methodInfo.DeclaringType);
                        clientMethod.ReturnType = new CodeTypeReference(methodInfo.ReturnType);
                        foreach (var parameterInfo in methodInfo.GetParameters())
                        {
                            clientMethod.Parameters.Add(new CodeParameterDeclarationExpression(parameterInfo.ParameterType, parameterInfo.Name));
                        }


                        if (methodInfo.ReturnType == typeof(void))
                        {
                            clientMethod.Statements.Add(
                                new CodeExpressionStatement(
                                    InvokeExecute(methodInfo)
                                    )
                                );
                        }
                        else if (methodInfo.ReturnType == typeof(Task))
                        {
                            clientMethod.Statements.Add(
                                new CodeMethodReturnStatement(
                                    InvokeExecuteTask(methodInfo)
                                    )
                                );
                        }
                        else if (methodInfo.ReturnType.IsGenericType && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
                        {
                            clientMethod.Statements.Add(
                                new CodeMethodReturnStatement(
                                    InvokeExecuteTask(methodInfo)
                                    )
                                );
                        }
                        else
                        {
                            clientMethod.Statements.Add(
                                new CodeMethodReturnStatement(
                                    new CodeCastExpression(
                                        methodInfo.ReturnType,
                                        InvokeExecute(methodInfo)
                                        )
                                    )
                                );
                        }
                    }
                }

                List<Type> interfaceTypes = new List<Type>();
                CollectType(interfaceType, interfaceTypes);

                string[] assemblies = new string[] { typeof(StrongTypedNodeEndpointClient).Assembly.Location }
                    .Concat(typeof(StrongTypedNodeEndpointClient).Assembly.GetReferencedAssemblies().Select(n => Assembly.Load(n).Location))
                    .Concat(typeof(XElement).Assembly.GetReferencedAssemblies().Select(n => Assembly.Load(n).Location))
                    .Concat(interfaceTypes.Select(t => t.Assembly.Location))
                    .Concat(interfaceTypes.SelectMany(t => t.Assembly.GetReferencedAssemblies().Select(n => Assembly.Load(n).Location)))
                    .Where(s => s != null)
                    .Distinct()
                    .ToArray();

                CompilerParameters options = new CompilerParameters();
                options.GenerateExecutable = false;
                options.GenerateInMemory = true;
                options.IncludeDebugInformation = false;
                options.ReferencedAssemblies.AddRange(assemblies);

                CompilerResults result = codedomProvider.CompileAssemblyFromDom(options, codeCompileUnit);
                codedomProvider.Dispose();
                clientType = result.CompiledAssembly.GetType(namespaceName + "." + typeName);
                clientImplementations.Add(interfaceType, clientType);
            }
            return clientType;
        }

        private static void CollectType(Type interfaceType, List<Type> types)
        {
            if (!types.Contains(interfaceType))
            {
                types.Add(interfaceType);
                foreach (var type in interfaceType.GetInterfaces())
                {
                    CollectType(type, types);
                }
                if (interfaceType.IsGenericType)
                {
                    foreach (var type in interfaceType.GetGenericArguments())
                    {
                        CollectType(type, types);
                    }
                }
            }
        }

        public static MethodInfo[] GetMethodInfos(Type interfaceType)
        {
            var methodInfos = new Type[] { interfaceType }
                .Concat(interfaceType.GetInterfaces())
                .Where(t =>
                    t != typeof(INodeEndpointClient) &&
                    t != typeof(IDisposable) &&
                    (!t.IsGenericType || t.GetGenericTypeDefinition() != typeof(IDuplexNodeEndpointClient<>))
                    )
                .SelectMany(t => t.GetMethods())
                .Where(m => !m.IsSpecialName)
                .ToArray();
            return methodInfos;
        }

        public static T Create<T>(INodeEndpointClientProvider provider)
            where T : INodeEndpointClient
        {
            Type clientType = CreateClientImplementation(typeof(T));
            return (T)clientType
                .GetConstructor(new Type[] { typeof(INodeEndpointClientProvider) })
                .Invoke(new object[] { provider });
        }
    }
}
